MCP application profile: custom content resources via static mcpResources#1613
Conversation
Component authors can now expose arbitrary text/blob content under author-chosen URIs on the application profile, parallel to mcpTools (#622) and mcpPrompts: entries declare a fixed uri or an RFC-6570-style uriTemplate ({name} one segment, {+name} cross-segment), served by an instance method dispatched on the LIVE registry class, with optional per-parameter completion values and resources list_changed support. Exported-Resource descriptors now list under harper+rest:// (the spec reserves https:// for web-fetchable resources); legacy http(s):// URIs still read and subscribe. Reserved schemes are rejected in mcpResources declarations so authors cannot shadow the built-in surfaces. Also fixes a latent registration gap: component entry loading is asynchronous past the boot awaits, so tableless components' custom mcpTools/mcpPrompts/mcpResources could register after the initial walk with no schema event to refresh it. Resources gains a monotonic registrationVersion and the MCP transport rebuilds lazily per request when the registry moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements MCP custom-resource registries, enabling component authors to publish custom content resources under custom URIs or URI templates. It also transitions exported-Resource descriptors to use a new harper+rest:// scheme instead of https:// for better spec compliance, while maintaining backward compatibility. Feedback on the changes highlights a potential issue with duplicate parameter names in URI templates that should be validated, and a bug in the unit tests where the legacy URI compatibility test incorrectly uses the new scheme instead of https://.
|
Reviewed; no blockers found. |
…om scheme
Codex final review: the lazy per-request rebuild could change the tool
set without emitting notifications/tools/list_changed (only prompts and
resources were notified), and the reserved-scheme guard could be
bypassed with a parameterized scheme position like {scheme}://{+path}.
The notifier per-session diffs, so no-op rebuilds stay silent; schemes
must now be literal and non-reserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e a legacy URI
Gemini bot review: docs:///{name}/{name} silently overwrote the first
captured value; and the scheme back-compat test requested harper+rest
for both cases (an overzealous scheme-migration sed), so the legacy
http(s) path was untested.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
guessAppHttpUrlPrefix appended the raw HTTP_PORT/HTTP_SECUREPORT config value as if it were a bare port, producing authorities like harper+rest://127.0.0.9:127.0.0.9:9926 when the port was configured as a bind address (host:port). Extract just the port. Pre-existing (legacy https:// descriptors were equally malformed); surfaced by runtime verification of #1609 and rolled into this PR at the user's request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This nails the functional shape we filed #1609 for — the One thing to lock down for that use case, since #1609's premise is a public docs server — i.e. anonymous / unauthenticated clients:
Could you confirm anonymous |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Custom resources impose no auth of their own (listing is per-profile, reads delegate to the author's method — parity with mcpTools' visibleTo: () => true). The new integration case runs a full no-Authorization-header session end-to-end; the registry doc no longer implies a login gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Confirmed: anonymous Addressed in 0a9083e:
Comment generated by an LLM (Claude Fable 5). |
…1610) Session-scoped buckets are trivially cycled by an anonymous client (initialize, call, drop session, repeat) and reset on restart — no cost control for public unauthenticated tools. Two additions, both opt-in: - Per-client-identity token buckets (rateLimit.ts): keyed on client identity (socket IP, or the first value of a config-named trusted header for proxied deployments), surviving session cycling. Config perClientPerSecond/perClientBurst per profile, default off; burst floors at one whole token so fractional sustained rates still admit; per-profile config now cached with a 10s TTL to keep the per-call identity resolution off the env-lookup path; startup warning when an identity header is configured (spoofing trap). - Durable quota hook (quota.ts): mcp.<profile>.quota.resource names an exported Resource whose static method (default allowMcpCall) is called per admitted tools/call with {identity, tool, user, profile, sessionId}; deny with {allowed:false, message?, retryAfterSeconds?} surfacing as kind:'quota_exceeded'. Fail-closed on misconfiguration or hook errors (#1422 precedent); dispatches on the live registry class; runs after the in-memory admit so throttled clients cannot spam a table-backed hook. Stacked on feat/mcp-custom-resources-1609 for the lazy registry freshness (#1613) that makes custom-tool fixtures deterministic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase onto the #1613 branch had replayed the intervening v5.1 release commits (including the #1575 revert) as head-side commits, polluting the PR diff; the branch now carries only this feature's commit. The misconfiguration warn-once flag is per profile so a second misconfigured profile still logs (gemini bot). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed via Claude review-queue (full review: harper-1613-0a9083e.md). Solid third leg of the MCP custom-surface triad (mcpResources, after mcpTools/mcpPrompts) — the reserved-scheme guard, RBAC delegation posture, error sanitization, and lazy freshness-rebuild fix are all well done and well tested. One real gap worth a look: Suggested fix: validate the raw captured segment has no |
{name} captures ran against the still-encoded URI, so %2F/%5C slipped
through the [^/]+ segment class and decoded to real separators after
the boundary check — defeating the one-segment contract authors rely
on for path construction (kriszyp review). Such URIs now simply fail
to match; {+name} reserved expansion still decodes them by design.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Great catch — fixed in a49b8b1. Comment generated by an LLM (Claude Fable 5). |
kriszyp
left a comment
There was a problem hiding this comment.
Reviewed via Claude review-queue — the {name} decode-order path-traversal fix is confirmed and empirically re-verified. CI red is a pre-existing unrelated main issue. LGTM.
Fixes #1609.
Summary
Component authors can now expose arbitrary
resources/read-able content — the issue's docs-server case: every documentation page as an MCP resource — viastatic mcpResourceson a Resource class, completing the custom-surface triad withmcpTools(#622) andmcpPrompts(#1349 §3.5):components/mcp/customResourceRegistry.ts, mirrors promptRegistry): fixed URIs list inresources/list, templates inresources/templates/list;{name}matches one segment,{+name}crosses segments (RFC-6570-lite, matching how clients expand templates); fixed match wins over templates.resources.jssubclass carries the author's access control; RBAC is delegated to the Resource). Returns: string → text,{text}/{blob,mimeType}pass through, other objects serialize as JSON. Author errors are sanitized (raw error only in the server log).completion/complete) serves author-declared per-parameter values, selected by the client'sref.uri.Descriptor scheme change (issue's "minor, related", user-approved): exported-Resource descriptors now list under
harper+rest://— the spec reserveshttps://for resources a client can fetch from the web, and these resolve in-process. Legacyhttp(s)://URIs continue to read and subscribe (back-compat). CustommcpResourcesmay not use reserved schemes (harper:,harper+rest:,http(s):) — prevents shadowing built-ins likeharper://schema/....Latent registration gap fixed en route: component entry loading is asynchronous past the boot awaits (chokidar's initial scan completes after
loadComponentDirectoriesresolves), so a tableless component's custommcpTools/mcpPrompts/mcpResourcesregistered after the initial walk and — with no schema event — never surfaced.Resourcesgains a monotonicregistrationVersion(bumped on set/delete) and the MCP transport rebuilds lazily per request when the registry moved (integer compare otherwise). This was reproduced by the integration test before the fix.Where to look
resources/Resources.tsset/delete and a per-request check intransport.handleMcpRequest.registerApplicationToolsis synchronous and idempotent; the version snapshot is taken before the walk so mid-walk registrations re-walk on the next request.readResource: registered author URIs win over the discovered surface. Reserved-scheme rejection at registration keeps built-ins unshadowable.guessAppHttpUrlPrefix()produced a malformed authority when the port config used thehost:portbind form (e.g.harper+rest://127.0.0.2:127.0.0.2:9926) — pre-existing, surfaced by runtime verification, fixed here (normalizePortForUrl) at the user’s request and verified live.Testing
36 new unit tests (registry template compilation/matching/completions; resources surface incl. blob/JSON/error normalization and scheme back-compat; registration-walk validation incl. reserved schemes and live-class dispatch) + 7 integration tests on a real instance with a tableless docs fixture (list/templates/read text+blob+template, sanitized author errors, completion). All 436 MCP unit tests and the sdk/row-authz/sse-listchanged integration suites pass — no regressions from the scheme change.
test:unit:mainhanded to CI (local dev instance holds the DB lock).Generated by an LLM (Claude Fable 5), cross-reviewed by Codex (subscribe-scheme + template-diff regressions caught and fixed) and Gemini (freshness undefined-version loop + reserved-scheme shadowing caught and fixed); the final Codex pass caught two more (lazy rebuilds not emitting tools/list_changed; a parameterized-scheme bypass of the reserved-scheme guard), both fixed in 3af972e.
Docs: companion documentation PR — HarperFast/documentation#567.